#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
#
#  lin-algebra-lib    Matrix manipulation library for Python 3
#  
#  Copyright 2014 John Hauser <phoenix.someone@gmail.com>
#  HIP 97649
#
#  Version 0.2
#
#  This library is free software; you can redistribute it and/or modify
#  it under the terms of the GNU Lesser Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU Lesser General Public 
#  License along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#
# This library contains documentation about its functions and overall 
# behaviour. This documentation is available at:
#	https://code.google.com/p/lin-algebra-lib/
#
# Research purposes only. Use Numpy if you want a reliable, working 
# matrix module.
#
# note that [3,4,34,5] is not a matrix. It could be either 
# [[3],[4],[34],[5]] or [[3,4,34,5]]. Feeding lists into these functions
# will cause errors.
#

"""

#                     ####START OF MODULE####

#Version number
versionNum=0.2

#Other modules that should be included

import cProfile
import math
import copy
import csv


#Variable prototypes
#UNTESTED
class class_Matrix():
	"""matrix object for matrixlib.py
Data from this object can be accessed by using the following names:
	__name__.isSanitized
	__name__.isTrimmed
	__name__.rows
	__name__.cols
	__name__.data
	__name__.type
"""
	isSanitized=False
	isTrimmed=False
	trimmedMode=""
	rows=0
	cols=0
	type="__Matrix__"
	def __init__(self, matrix_prototype):
		self.rows=len(matrix_prototype)
		self.cols=len(matrix_prototype[0])
		self.data=matrix_prototype
		return None
	def appendHorizontal(self, matrixObject):
		if not (matrixObject.type!="__Matrix__" or matrixObject.type!="__Vector__"):
			raise TypeError("Input variable is not a Matrix or Vector object.")
		if self.cols!=matrixObject.cols:
			raise ShapeError("Input object does not have the same amount of columns ("+str(self.cols)+"to"+str(matrixObject.cols)+").")
		for i in range(0, self.cols):
			self.data[i].append(matrixObject[i])
		return None
	def appendVertical(self, matrixObject):
		if not (matrixObject.type!="__Matrix__" or matrixObject.type!="__Vector__"):
			raise TypeError("Input variable is not a Matrix or Vector object.")
		if self.rows!=matrixObject.rows:
			raise ShapeError("Input Matrix object does not have the same amount of rows ("+str(self.rows)+"to"+str(matrixObject.rows)+").")
		self.data.append(matrixObject.data)
		return None
	def echelonReduce(self, ):
		return None

#UNTESTED
class class_nDimDataSet():
	"""Dataset object for matrixlib.py
Data from this object can be accessed by using the following names:
	__name__.isSanitized
	__name__.isTrimmed
	__name__.dimension
	__name__.length
	__name__.data
	__name__.type
"""
	isSanitized=False
	isTrimmed=False
	trimmedMode=""
	dimension=0
	length=0
	type="__nDimDataSet__"
	def __init__(self, dataset_prototype):
		self.dimension=len(dataset_prototype[0])
		self.length=len(dataset_prototype)
		self.data=dataset_prototype		
		return None
	def appendData(self, dataset):
		if matrixObject.type!="__nDimDataSet__":
			raise TypeError("Input variable is not a Dataset object.")
		if self.dimension!=matrixObject.dimension:
			raise DimensionError("The two datasets do not have the same dimensions.")
		self.data.append(matrixObject.data)
		return None

class class_Vector():
	"""vector object for matrixlib.py
Data from this object can be accessed by using the following names:
	__name__.isSanitized
	__name__.isTrimmed
	__name__.rows
	__name__.data
	__name__.type
Please note that this object automatically sanitizes and trims itself,
and it is always constructed from a single list.

Non-numerical data in the list throws a ValueError exception.
"""
	isSanitized=False
	isTrimmed=False
	rows=0
	type="__Vector__"
	def __init__(self, vector_prototype):
		self.rows=len(matrix_prototype)
		vector_prototype=[float(i) for i in vector_prototype]
		self.isSanitized=True
		self.isTrimmed=True
		self.data=vector_prototype
		return None



#Module global variables (empty)





#Exception handlers.

#I feel this is incomplete, IMO.
class ShapeError(Exception):
	def _init_(self, value):
		self.value=value
	def _str_(self):
		return repr(self.value)

class SizeError(Exception):
	def _init_(self, value):
		self.value=value
	def _str_(self):
		return repr(self.value)

class DimensionError(Exception):
	def _init_(self, value):
		self.value=value
	def _str_(self):
		return repr(self.value)

class InputError(Exception):
	def _init_(self, value):
		self.value=value
	def _str_(self):
		return repr(self.value)


###TEST VARIABLES###

#MATRIX CLASS#
mat_square=class_Matrix([[6,4,3.0,6,3],[4,4,1,4,2],[6,9,4,5,2],[6,9,23,4,4],[3,3,4,5,6]])
mat=class_Matrix([[4,5.0,4],[4,5,4],[4,5,4],[5,6,4]])
mat_unsanitized=class_Matrix([["HI", "3",3,4,5],[False,2,3],[],["HI",3,4,5],[2,1,-4],[4.25436]])
mat_malformed=class_Matrix([[1,2,3,4,5,6,7,8,9,10],[1,2,3],[1,2,3,4,5,6,7],[1],[1,2,3,4]])
mat_broken=class_Matrix([[1,2,3,4,"\n",7,8,9,10],[],[1,2,3,4,5,6,7],[1],[1,2,3,"G"]])
#BOTH unsanitized and untrimmed

#DATASET CLASS#


#VECTOR CLASS#



#Checks out
def listAdd(a,b):
	A=len(a)
	B=len(b)
	if A!=B:
		raise SizeError("Mismatched list length")
	c=[a[i]+b[i] for i in range(0, A)]
	return c

#checks out.	
def listMult(a,b):
	A=len(a)
	B=len(b)
	if A!=B:
		raise SizeError("Mismatched list length")
	c=[a[i]*b[i] for i in range(0, A)]
	return c

#Tests out
def formatNumList(List, Format, csv=False):
	"""formatNumList(List, Format, csv=False):
Formats a list of numbers to be used in printMatrix. Can be called on 
its own. Note that it accepts lists as input only. Matrix input will 
crash the function.
"""
	string=""
	for x in List:
		string=string+", "+Format.format(x)
	if csv==False:
		string="["+string[1:]+"]"
	elif csv==True:
		string=string[1:]
	return string

#Works
def printMatrix(matrix, Format="", csv=False):
	"""printMatrix(matrix, Format="", bool(csv)=False):
Takes a Matrix or nDimDataSet object and prints the object to console,
trying to align it so that all the elements form a grid in the terminal.

The Format option is used to determine the formatting of all elements in
the matrix. The format options that c uses should be fed into this 
function (e.g. 10d, 10.4f)
"""
	if not (matrix.type=="__Matrix__" or matrix.type=="__nDimDataSet__"):
		raise TypeError("object must be Matrix or nDimDataSet.")	
	#if matrix.isSanitized!=True:
	#	raise InputError("object must be sanitized before input.")
	Format="{:"+ Format+"}"
	Format.format(42.00)
	formattedData=[]
	if csv==False:
		if matrix.rows!=1:
			print('[' + formatNumList(matrix.data[0],Format) + ',')
			for i in range(1, len(matrix.data)-1):
				print(' ' + formatNumList(matrix.data[i],Format) + ',')
			print(' ' + formatNumList(matrix.data[-1],Format) + ']')
		if matrix.rows==1:
			print("["+formatNumList(matrix.data[0],Format)+"]")
	else:
		if matrix.rows!=1:
			print(formatNumList(matrix.data[0],Format,True))
			for i in range(1, len(matrix.data)-1):
				print(formatNumList(matrix.data[i],Format,True))
			print(formatNumList(matrix.data[-1],Format,True))
		if matrix.rows==1:
			print(formatNumList(matrix.data[0],Format,True))
	return None

#Note the try statement is called for every element.
#It could be called once for every caught statement
#USE MTIMEIT
#Works
def listSanitize(List, rownum, isFloat=True):
	if isFloat==True:
		col1=0
		success=True
		if len(List)==0:
			print("Row "+str(rownum+1)+" is empty.")
		else:
			for i in range(col1, len(List)):
				try:
					List[i]=float(List[i])
				except (TypeError, ValueError):
					success=False
					print("Element at ("+str(rownum+1)+", "+str(i+1)+") is not a number.")
					#NOTE: when printing values, the positions are actually offset by one
					#this is because matrix indexing in math starts at 1, not 0.
		if success==True:
			return List
		elif success==False:
			return None
	elif isFloat==False:
		col1=0
		success=True
		if len(List)==0:
			print("Row "+str(rownum+1)+" is empty.")
			success=False
		else:
			for i in range(col1, len(List)):
				try:
					List[i]=int(List[i])
				except (TypeError, ValueError):
					success=False
					print("Element at ("+str(rownum+1)+", "+str(i+1)+") is not a number.")
					#NOTE: when printing values, the positions are actually offset by one
					#this is because matrix indexing in math starts at 1, not 0.
		if success==True:
			return List
		elif success==False:
			return None

#Works
def matrixSanitize(matrix, isFloat=True):
	"""matrixSanitize(matrix, bool(isFloat)=True):
Takes a Matrix or nDimDataSet object and transforms all the elements in
it into floats or integers, depending on the value of isFloat. It will
fail and print the element that is not a number so the user can repair it.

Does not return a new object, it simply repairs the one supplied as an 
argument.

Note: listSanitize() is required for this function to work
"""
	if not (matrix.type=="__Matrix__"or matrix.type=="__nDimDataSet__"):
		raise TypeError("object must be Matrix or nDimDataSet.")	
	matrix.isSanitized=False
	successList=[1]*matrix.rows
	#Keeps track of row correctness. Incorrect until proven otherwise
	for i in range(0, matrix.rows):
		a=listSanitize(matrix.data[i], i, isFloat)
		if a!=None:
			matrix.data[i]=a
			successList[i]=0
	if sum(successList)==0:
		matrix.isSanitized=True
	return None

###TESTING SUITE FOR matrixSanitize
"""
printMatrix(mat_unsanitized)
mat_unsanitized.isSanitized=True
print(mat_unsanitized.isSanitized)
matrixSanitize(mat_unsanitized)
print(mat_unsanitized.isSanitized)
#"""

#untested
def matrixTrim(matrix, mode="fail"):
	"""matrixTrim(matrix, mode="short") in matrixlib.py
this function takes a Matrix or nDimDataSet object and trims it
on one of five modes:
	-'short':take the shortest row on the object and trim to that
	-'large': take the largest row on the object and add zeroes to the other
	 rows until they all match the largest original size 
	-'average' or 'avg': take the average and trim (or add) to that
	-'fail': the important one: fails with ShapeError if the matrix is not 
	 trimmed corrrectly
	-numerical: the most important one: trims (or adds zeroes) until all
	 rows have the size indicated (mode must be an int for this to succeed.)
OUTSTANDING ISSUES: 
----mode="fail" is untested
----object.data list manipulations are piss poor in fail and numerical modes.
"""
	if not (matrix.type=="__Matrix__" or matrix.type=="__nDimDataSet__"):
		raise TypeError("object must be Matrix or nDimDataSet.")
	if mode=="fail":#Works
		lengthList=[len(i) for i in matrix.data]
		valMin=min(lengthList)
		lengthList=[i-valMin for i in lengthList]
		for i in lengthList:
			if i!=0:
				matrix.isTrimmed=False
				raise ShapeError("input object is not trimmed correctly.")
		matrix.isTrimmed=True
		#overly simple, but easy to program. DOnt know if it would be more efficient.
	elif type(mode)==int:
		lengthList=[len(i) for i in matrix.data]
		minVal=min(lengthList)
		average=(sum(lengthList)/len(lengthList))
		minNum=int(round(average))-minVal
		matrix.data=[i.append([0]*minNum) for i in matrix.data]
		matrix.data=[i[0:mode] for i in matrix.data]
		#FLAG. THIS CODE MAY BE QUITE SHITTY
		matrix.cols=mode
		matrix.isTrimmed=True
	elif mode=="short":
		lengthList=[len(i) for i in matrix.data]
		minVal=min(lengthList)
		matrix.data=[i[0:minVal] for i in matrix.data]
		matrix.cols=minVal
		matrix.isTrimmed=True
	elif mode=="large":
		lengthList=[len(i) for i in matrix.data]
		maxVal=max(lengthList)
		matrix.data=[i+[0]*(maxVal-len(i)) for i in matrix.data]
		matrix.cols=maxVal
		matrix.isTrimmed=True
	elif mode=="average":
		lengthList=[len(i) for i in matrix.data]
		minVal=min(lengthList)
		average=(sum(lengthList)/len(lengthList))
		minNum=int(round(average))-minVal
		matrix.data=[i.append([0]*minNum) for i in matrix.data]
		matrix.data=[i[0:average] for i in matrix.data]
		#FLAG. THIS CODE MAY BE QUITE SHITTY
		matrix.cols=average
		matrix.isTrimmed=True
	else:
		raise InputError("'mode' input variable had an incorrect value.")
	return matrix

#matrixTrim testing suite.
"""
printMatrix(mat_square)
mat_square.isTrimmed=False
print(mat_square.isTrimmed)
matrixTrim(mat_square, 3)
print(mat_square.isTrimmed)
#"""

#Tests out
def nullMatrix(n,m):
	"""nullMatrix(int(n),int(m)) for matrixlib.py
This library generates a n by m matrix object full of zeroes. Thats 
about it, really.
"""
	if (n==0 or m==0):
		raise SizeError("Null matrix must have nonzero rows or columns")
	if (n+m)%1!=0:
		raise TypeError("inputs must be int.")	
	line=[0]*n
	matrix=[line]*m
	matrix=class_Matrix(matrix)
	matrix.isSanitized=True
	matrix.isTrimmed=True
	return matrix


#Works
def inputMatrix(char=" ", isMatrix=True):
	"""inputMatrix(str(char),bool(isMatrix)=True) len(char=1)
Opens a text-based interface to create matrix or nDimDataSet objects. 
Each row is typed in a line of input (so rows are separted by newlines).
char is a character value that separates each element in the row.
Pressing newline twice will signal the end of data collection and return
the matrix or nDimDataSet object, call matrixSanitize on that object,
and then return it, or crash matrixSanitize and indicate the non-numeric
elements.
"""
	if (type(char)!=str or len(char)!=1):
		#Short circuiting prevents TypeError if char is not iterable.
		raise InputError("char must be a string with length=1")
	print("Input matrix as a series of rows. Elements are separated by '"+ str(char) +"'.") 
	print("Rows are separated by newlines. Type EOF or newline twice to end.")
	n=0
	matrix=[]
	i=0
	while True:#counter.
		i=+1
		try:
			inputRow=input()
		except EOFError:
			break
		A=len(inputRow)
		list1=[]
		if (A=='\n'): break
		inputRow=inputRow.split(str(char))
		try:
			list1=[float(i) for i in inputRow]#TEST PLEASE
			matrix.append(list1)			
		except TypeError:
			print('TypeError: non-numerical character on input. Repeat the row.')
		n=+1
	if i==1:
		print("Input cancelled")
		return None
	if isMatrix==True:
		matrix=class_Matrix(matrix)
	else:
		matrix=class_nDimDataSet(matrix)
	matrix=matrixSanitize(matrix)
	return matrix

#Finished. Cannot handle \n\n or comments, but not sure if those
#should even be there.
def openCSVmatrix(PATH, isMatrix=True):
	"""openCSVmatrix(str(PATH), bool(isMatrix)=True):
This function opens a csv file with the PATH string, and feeds the data 
in the file into a Matrix or nDimDataSet object. 
"""
	matrix=[]
	csvFile=open(str(PATH))
	csvObject=csv.reader(csvFile, delimiter=' ')
	oldRow=0
	Warn=False
	for csvLine in csvObject:
		row=csvLine[0].split(",")
		if (oldRow!=len(row) and oldRow!=0): Warn=True
		matrix.append([float(element) for element in row])
		oldRow=len(row)
	if Warn==True: print("openCSVmatrix(): warning: matrix rows do not have same length.")
	if isMatrix==False:
		matrix=class_Matrix(matrix)
	else:
		matrix=class_Matrix(matrix)
	return matrix

#Profile both modes for this function
#Credit to the Python Foundation code examples for this code.
def transpose(matrix):
	"""transpose(matrix):
Takes a Matrix object and returns a transposed Matrix object.
"""
	if matrix.type!="__Matrix__":
		raise TypeError("object must be Matrix")
	if matrix.rows==matrix.cols:
		matrixa=[[row[i] for row in matrix.data] for i in range(0, matrix.rows)]
		#^implicit double for loop. Very obfuscated, though.
		matrixa=class_Matrix(matrixa)
		return matrixa
	else:
		list1=[0]*matrix.rows
		matrixa=list1*matrix.cols
		matrixa=[[row[i] for row in matrix.data] for i in range(0,matrix.cols)]
		#^implicit double for loop. Very obfuscated, though.
		matrixa=class_Matrix(matrixa)
		return matrixa

#Untested
def matrixAdd(a, b):
	"""matrixAdd(a, b):
takes two Matrix or nDimDataSet objects and adds them together, returning
a Matrix or nDimDataSet object.

The output object will be the same class as the two input objects. If 
they are different, the output object will be always a Matrix object.
"""
	if not (a.type=="__Matrix__" or a.type=="__nDimDataSet__"):
		raise TypeError(" first object must be Matrix or nDimDataSet.")	
	if not (b.type=="__Matrix__" or b.type=="__nDimDataSet__"):
		raise TypeError(" first object must be Matrix or nDimDataSet.")	
	if (a.rows!=b.rows or a.cols!=b.cols):
		raise ShapeError('Matrix shapes do not match.')
	if a.type=="__Matrix__":
		A=a.rows
	if a.type=="__nDimDataSet__":
		A=a.length
	try:
		c=[listAdd(a.data[i], b.data[i]) for i in range(0, a.rows)]
	except (IndexError, ShapeError):
		raise IndexError("The two input objects do not have the same dimensions.")
	if (a.type=="__nDimDataSet__" and b.type=="__nDimDataSet__"):
		c=class_nDimDataSet(c)
	elif (a.type=="__Matrix__" and b.type=="__Matrix__"):
		c=class_Matrix(c)
	else:
		c=class_Matrix(c)
	return c

#UNTESTED
#NOTE THIS FUNCTION MUST COPY THE MATRIX FIRST BY DEFAULT.
def matrixMult(matrixa, matrixb, lowMem=False):
	"""matrixMult(matrixa, matrixb, lowMem=False):
Takes two Matrix objects and mutiplies them together, returning a third
Matrix object.

lowMem has two options: 
-False forces the second Matrix object to be transposed, copying it 
 first. This makes the implementation easier and MAY save on processor 
 cycles, but consumes 50% more memory
-True: This uses the second Matrix object without changing it. The
implementation is not done yet for this mode.
"""
	if not (matrixa.type=="__Matrix__" or matrixb.type=="__Matrix__"):
		raise TypeError("object must be Matrix")
	if matrixa.rows!=matrixb.cols:
		raise ShapeError("Matrices do not have the correct shape.")
	if lowMem==False:
		matrixb=transpose(matrixb)
		#THis bit creates the shape of matrixc
		zeroList=[0]*matrixb.rows
		matrixc=[zeroList]*matrixa.rows
		for i1 in range(0, matrixa.rows):
			list1=[0]*matrixb.rows#DONT ERASE. NECESSARY. OTHERWISE PYTHON DOESNT WORK FOR SOME REASON
			for i2 in range(0, matrixb.rows):
				list1[i2]=(sum(listMult(matrixa.data[i1],matrixb.data[i2])))
			matrixc[i1]=list1#DONT ERASE. NECESSARY. OTHERWISE PYTHON DOESNT WORK FOR SOME REASON
		matrixc=class_Matrix(matrixc)
		return matrixc
	else:
		#UNIMPLEMENTED
		return None

#Untested
def identityMatrix(size):
	"""identityMatrix(int(size)):
Creates a Matrix object that contains an identity matrix of size*size.
Note: the Matrix object will be marked as sanitized and trimmed by 
default.
"""
	matrix=[[0]*(size) for i in range(0, size)]
	for i in range(0, size):
		matrix[i][i]=1
	matrix=class_Matrix(matrix)
	matrix.isSanitized=True
	matrix.isTrimmed=True
	return matrix

#Untested
def addIdentityMatrix(matrix):
	"""addIdentityMatrix(matrix):
Takes a Matrix object and appends an indentity matrix to it. The 
identity matrix is appeded to the "right" of the input matrix.

The output is also a Matrix object.
"""
	if matrix.type!="__Matrix__":
		raise TypeError("object must be Matrix.")	
	if matrix.rows!=matrix.cols:
		raise ShapeError('Input matrix is not square.')
	#UPDATE ROWS AND COLS DATA
	idRowClean=[]
	idRowClean=[0]*(matrix.cols-1)
	abb=matrix.data[0]+idRowClean[:matrix.rows-1]#Why is this here?
	for i in range(0,matrix.rows):
		matrix.data[i]=matrix.data[i]+idRowClean
		matrix.data[i].insert(i+matrix.cols, 1)
	#UPDATE ROWS AND COLS DATA
	return matrix

#Untested
def mergeMatrices(matrixa, matrixb, horizontal=True):
	"""mergeMatrices(matrixa, matrixb):
Takes two Matrix objects and concatenates them together, returning 
another Matrix object, that represents an augmented matrix. Vector
objects are also accepted.
"""
	if not (matrixa.type=="__Matrix__" or matrixa.type=="__Vector__"):
		raise TypeError("First object must be a Matrix or Vector object")	
	if not (matrixb.type=="__Matrix__" or matrixb.type=="__Vector__"):
		raise TypeError("Second object must be a Matrix or Vector object")	
	if horizontal=True:
		return matrixa.appendHorizontal(matrixb)
	if horizontal=False:
		return matrixa.appendVertical(matrixb)
	return None


#Untested
#OUTSTANDING ISSUES
def echelonReduce(matrix, right=False, ones=True):
	"""echelonReduce(matrix, right=False, ones=True):
Performs Gauss-Jordan elimination on a Matrix object. It can take any 
size of matrix, as long as the number of columns is larger or equal to 
the number of rows.

After calling this function, the matrix object will be treated as either
a square matrix or an augmented matrix. As the elimination proceeds, the
Matrix object will remain with a square area that is similar to an 
identity matrix , in which the diagonal can be either full of ones
(ones=True), or with real numbers (ones=False)

The ones=False option is designed to use the echelonReduce function to
calculate determinants.

The right option is used to determine where in the matrix object the
square identity matrix appears (on the right side or left side of the
Matrix object.)

OUTSTANDING ISSUES:
-it has not been decided whether this function should be called as a 
 standard function or a method on Matrix objects (check PENDING_CHANGES)
-Right=True is unimplemented
"""
	if matrix.type!="__Matrix__":
		raise TypeError("object must be Matrix")
	if right==False:
		if matrix.cols<matrix.rows:
			raise ShapeError('Number of columns in augmented matrix is too small.')#TEST
		#THese prevent all the index shenanigans later	
		for echelonRowIndex in range(0, matrix.rows):
			#might break in weird ways here ^
			echelonRow=matrix.data[echelonRowIndex]#echelonRow is the row the proportionality constant acts on
			echelonRow1=copy.deepcopy(echelonRow)
			for rows in range(0, matrix.rows):#could replace rows with the matrix variable, since it is iterable. Might simplify some code.
				K=(-1)*(matrix.data[rows][echelonRowIndex]/echelonRow[echelonRowIndex])#dont know if it is correct
				echelonRow1=[i*K for i in echelonRow]
				matrix.data[rows]=listAdd(echelonRow1, matrix.data[rows])
			matrix.data[echelonRowIndex]=echelonRow #Note that the row that was the reference was also changed. Gotta change this.
		if ones==True:
			for ind in range(0, matrix.rows):
				x=matrix.data[ind][ind]
				if x!=0:
					matrix.data[ind]=[float(i/x) for i in matrix.data[ind]]
		else:
			for ind in range(0, matrix.rows):
				matrix.data[ind]=[float(i) for i in matrix.data[ind]]			
		#MUST IMPLEMENT WAY TO ELIMINATE NEGATIVE ZEROES.		
		matrix=class_Matrix(matrix)
		return matrix
	if right==True:
		print('unimplemented')
		return None

#Untested
#Note: returns a list which contains the Matrix objects
#NOTE: USE PRINTMATRIX TO GENERATE MATRICES AND ILLUSTRATE THIS IN THE DOCSTRING.
#NOTE: right=False is UNIMPLEMENTED
def splitAugmentedMatrix(matrix, right=True):
	"""splitAugmentedMatrix(matrix, right=True):
Takes an augmented matrix and splits it, returning two matrix objects, 
so that one of the two matrix objects is a square matrix. This function
is designed to split augmented matrices, so that the user can pick only
the part of the augmented matrix that the user wants to analyze, or to
pluck off identity matrices after calling echelonReduce.

The right option determines where the split will be made: Right=True 
forces the split to be in the right side of the original matrix (the 
square matrix will be in the left). Setting right=False will force the 
split to be in the left side of the original matrix (so the square 
matrix will be in the right)

(Maybe add a diagram here?)

Will throw ShapeError if the number of columns is smaller or equal to 
the number of rows (this will make splits that result in square matrices
impossible)
"""
	if matrix.type=="__Matrix__":
		raise TypeError("object must be Matrix.")	
	ans=[0,0]
	if matrix.cols<=matrix.rows:
		raise ShapeError('Matrix does not have correct shape (should have more columns than rows)')
	#The splitter
	if Right==True:
		ans1=[i[:matrix.rows] for i in matrix.data]
		ans2=[i[matrix.rows:] for i in matrix.data]
	else:#INDICES. WATCH OUT FOR THEM
		ans1=[i[:(matrix.cols-matrix.rows)] for i in matrix.data]
		ans2=[i[(matrix.cols-matrix.rows):] for i in matrix.data]
	ans[0]=class_Matrix(ans1)
	ans[1]=class_Matrix(ans2)
	return ans

#Untested
#OUTSTANDING ISSUES
def invertMatrix(matrix):
	"""invertMatrix(matrix)
Takes a Matrix object and inverts the matrix within it.

OUTSTANDING ISSUES:
-No sanity testing for matrices that are non-invertible.
"""
	if matrix.type!="__Matrix__":
		raise TypeError("Input object must be Matrix")	
	if matrix.rows!=matrix.cols:
		raise ShapeError('Matrix is not square')
	a=addIdentityMatrix(matrix)
	a=splitAugmentedMatrix(echelonReduce(a))#Uhh, may have issues?
	return a[1]


#Untested
#OUTSTANDING ISSUES
def determinant(matrix):
	"""determinant(matrix)
Finds the determinant of a square Matrix object by calling echelonReduce
with ones=False on the object, and then multiplying all the numbers from
the diagonal that remains.

Will return zero for non-invertible matrices.

OUTSTANDING ISSUES
-No sanity testing on echelonReduce for non-invertible matrices.
"""
	if matrix.type!="__Matrix__":
		raise TypeError("Input object must be Matrix")	
	if matrix.rows!=matrix.cols:
		raise ShapeError('Matrix is not square')
	a=echelonReduce(matrix, ones=False)
	b=[matrix.data[i][i] for i in range(0, matrix.rows)]
	a=1
	for i in b: a=a*i
	return a


#Untested
#MAY BE DEPRECATED 
def listToMatrix(List, cols, rows=None):
	"""
"""
	if rows==None:
		rows=(len(List)/cols)
		if rows%1!=0:
			raise ShapeError('Desired matrix shape results in incomplete rows.')
		rows=int(rows)
	else:
		if cols*rows!=len(List):
			raise ShapeError('Desired shape does not match with input list.')
	matrix=[List[i*cols:(i+1)*cols] for i in range(0, rows)]
	matrix=class_Matrix(matrix)
	return matrix

#wrapper for openCSVmatrix(). Stores data points as matrices
def openCSVdataset(PATH):
	dataset=openCSVmatrix(PATH)
	return dataset

#wrapper for openCSVdataset(), so that there are no confusions when calling functions
def openCSVdataSet(PATH):
	return openCSVdataset(PATH)

#untested
def displaceDataSet(dataset, vector):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")
	if vector.type!="__Vector__":
		raise TypeError("Displacement object must be Vector.")	
	if dataset.cols!=vector.rows:
		raise DimensionError('initial and final data sets have different dimensions')
	newDataset=[]
	for i in dataset:
		newDataset.append(listAdd(i, vector.data))
	newDataSet=class_nDimDataSet(newDataSet)
	return newDataSet



#Shell function
def transfRotate(dataset, rotate, radiansInput=True):
	"""transfRotate(dataset, rotate, radiansInput=True):
Takes a nDimDataSet object, and rotates all the points about the origin
by the angles specified in the rotate tuple. Note that tuple must be the
same length as the nDimDataSet object, even if one plans to rotate about
one axis only.

rotate is always in radians, though the input can be forced in radians 
instead.

This only rotates about the origin. To rotate around other points, 
translate so that the desired point is zero, rotate and then translate 
back so that origin become the original point.
"""
	return None

#INCOMPLETE
def transfStretch(dataset, vector):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")	
	ans=[]
	for i in dataset.data:
		ans.append(listMult(i, vector))
	return ans

#Incomplete
def transfShear(dataSet, shearAxis, shearFactor):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")	
	return newDataSet

#Untested.
#Can be used just the same as polynomialLeastSquares, though it is faster
#(I think)
def linearLeastSquares(dataset):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")	
	#Part1: create matrices Y and A from dataSet
	Y=[[i[1]] for i in dataset]#i[1] or 0?
	A=[[i[0]]+[1] for i in dataset]
	#Part2: calculate
	AT=transpose(A)
	fit=invertMatrix(matrixMult(AT,A))
	fit=matrixMult(fit, AT)
	fit=matrixMult(fit, Y)
	return fit


#Untested
#degree is the degree of the polynomial. 1 for linear, 0 for constant fit.
def polynomialLeastSquares(dataset, degree):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")	
	if type(degree)!=int:
		raise TypeError("degree of polynomial must be int.")
	#Part1: create matrices Y and A from dataSet
	Y=[[i[1]] for i in dataset]
	A=[[(i[0])**n for n in range(degree,-1,-1)] for i in dataset]
	printMatrix(A)
	#^Problem here: maybe implementation is too slow.
	#Part2: calculate
	AT=transpose(A)
	fit=invertMatrix(matrixMult(AT,A))
	fit=matrixMult(fit, AT)
	fit=matrixMult(fit, Y)
	return fit


def arbitraryLeastSquares(dataset, equation):
	if dataset.type!="__nDimDataSet__":
		raise TypeError("object must be nDimDataSet.")	
	#Part1: create matrices Y and A from dataSet
	Y=[[math.log((i[1])/2.56)] for i in dataset]#i[1] or 0?
	A=[[i[1]] for i in dataset]
	#Part2: calculate
	AT=transpose(A)
	fit=invertMatrix(matrixMult(AT,A))
	fit=matrixMult(fit, AT)
	fit=matrixMult(fit, Y)
	return fit

#FOR INTERACTIVE MODE
#OPTIONS:
#	use termios
#	use sysconfig to force interactive mode
#

def main():
	return 0

if __name__ == '__main__':
	main()

